/*
 * Copyright (C) 2010-2011 Comcast Cable.  All Rights Reserved.
 */
package com.hercules.android.receiver.handlers.events.keymappingservice;

import org.w3c.dom.Document;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

import android.content.Context;

import com.hercules.android.receiver.LogUtil;
import com.hercules.android.receiver.handlers.events.AndroidReceiverEventHandlersConfigurationException;
import com.hercules.android.receiver.handlers.events.EventsUtil;
import com.hercules.android.receiver.handlers.events.KeyMappingFileLoadingException;
import com.hercules.android.receiver.handlers.events.KeyMappingService;
import com.hercules.android.receiver.model.events.XREVirtualKeyCode;

import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Logger;
import java.util.regex.Pattern;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.FactoryConfigurationError;
import javax.xml.parsers.ParserConfigurationException;

/**
 * <p>
 * This is the default implementation of KeyMappingService. Upon getting a XREVirtualKeyCode, it checks if the mapping
 * file is loaded or not. If not, it loads the mapping XML file and store the mapping in this.keyMapping.
 * </p>
 * <p>
 * AndroidAssembly1 made this class to load from resource
 * <p>
 * <strong>Thread safety: </strong> This class is thread-safe after setter configuration because it's immutable after
 * setter configuration
 * </p>
 *
 * @author mekanizumu, jaindsonvs
 * @author 7realm
 * @version 1.0
 */
public class KeyMappingServiceImpl implements KeyMappingService {
    /**
     * <p>
     * Represents the pattern for a valid raw code value.
     * </p>
     * <p>
     * Initialization and Mutability: It is final and won't change once it is initialized.
     * </p>
     * <p>
     * Usage: It's is used in validateRawCode(String).
     * </p>
     */
    private static final Pattern RAW_CODE_PATTERN = Pattern.compile("0x[a-fA-F0-9]+");

    /**
     * <p>
     * The logger used for logging.
     * </p>
     * <p>
     * Initialization and Mutability: It is final and won't change once it is initialized as part of variable
     * declaration to: Logger.getLogger(KeyMappingServiceImpl.class.getName()).
     * </p>
     * <p>
     * Usage: It's used in all public methods.
     * </p>
     */
    private static final Logger LOGGER = Logger.getLogger(KeyMappingServiceImpl.class.getSimpleName());

    /**
     * The android Context.
     * <p>
     * Initialization and Mutability: It does not need to be initialized when the instance is created.
     * <p>
     * Usage: It is used in loadKeyMappingFile(), setContext(). Its value legality is checked in checkInitialization()
     * method.
     * <p>
     * AndroidAssembly1 new field
     */
    private Context context;

    /**
     * The resource ID of mapping file.
     * <p>
     * Initialization and Mutability: It does not need to be initialized when the instance is created.
     * <p>
     * Usage: It is used in loadKeyMappingFile(), setResourceId(). Its value legality is checked in
     * checkInitialization() method.
     * <p>
     * AndroidAssembly1 new field
     */
    private int resourceId;

    /**
     * <p>
     * The mapping between the raw code and shift/ctrl/alt/meta combination to a XREVirtualKeyCode (the key is an
     * encoded string, see encodeKeyCode()'s method documentation for its format).
     * </p>
     * <p>
     * Initialization and Mutability: It can be changed after it is initialized as part of variable declaration to:
     * null.
     * </p>
     * <p>
     * Usage: It is used in loadKeyMappingFile(), getVirtualKeyCode().
     * </p>
     */
    private Map<String, XREVirtualKeyCode> keyMapping = null;

    /**
     * Sets the android Context.
     *
     * @param context the android Context
     */
    public void setContext(Context context) {
        this.context = context;
    }

    /**
     * Sets the resource id of mapping file.
     *
     * @param resourceId the resource id of mapping file
     */
    public void setResourceId(int resourceId) {
        this.resourceId = resourceId;
    }

    /**
     * <p>
     * This is the default constructor for the class.
     * </p>
     */
    public KeyMappingServiceImpl() {
        // empty
    }

    /**
     * Get virtual key code from the raw key code and whether shift/ctrl/alt/meta keys are pressed.
     * <p>
     * AndroidAssembly1 changed to load from resource
     *
     * @param alt this is true if 'alt' is pressed.
     * @param rawCode the raw keyboard code.
     * @param ctrl this is true if 'ctrl' is pressed.
     * @param meta this is true if 'meta' is pressed.
     * @param shift this is true if 'shift' is pressed.
     * @return the mapped virtual key code. It could be null.
     * @throws KeyMappingFileLoadingException if any error occurs
     */
    public XREVirtualKeyCode getVirtualKeyCode(int rawCode, boolean shift, boolean ctrl, boolean alt, boolean meta)
        throws KeyMappingFileLoadingException {
        final String methodName = "getVirtualKeyCode";
        LogUtil.enter(LOGGER, this, methodName);

        XREVirtualKeyCode result = null;
        try {
            if (keyMapping == null) {
                InputStream inputStream = context.getResources().openRawResource(resourceId);
                try {
                    loadKeyMappingFile(inputStream);

                    LogUtil.info(LOGGER, this, methodName, "Key mappings successfully loaded from resource");
                } finally {
                    inputStream.close();
                }
            }
            result = keyMapping.get(encodeKeyCode(rawCode, shift, ctrl, alt, meta));
        } catch (IOException e) {
            throw LogUtil.error(LOGGER, this, methodName, new KeyMappingFileLoadingException(
                "I/O error occurred while reading key mapping resource file, id '" + resourceId + "'.", e));
        }

        return LogUtil.exit(LOGGER, this, methodName, result);
    }

    /**
     * <p>
     * Encode the raw key code and shift/ctrl/alt/meta combination into a string.
     * </p>
     *
     * @param alt this is true if 'alt' is pressed.
     * @param rawCode the raw keyboard code.
     * @param ctrl this is true if 'ctrl' is pressed.
     * @param meta this is true if 'meta' is pressed.
     * @param shift this is true if 'shift' is pressed.
     * @return the encoded string containing the information of the raw key code and shift/ctrl/alt/meta combination, in
     * the form of "shift={xxx}&ctrl={xxx}&alt={xxx}&meta={xxx}&rawCode={yyy}" where {xxx} is "true" or "false" and
     * {yyy} is the raw code which is a decimal string like "100".
     */
    private static String encodeKeyCode(int rawCode, boolean shift, boolean ctrl, boolean alt, boolean meta) {
        StringBuilder sb = new StringBuilder();
        sb.append("shift=");
        sb.append(Boolean.toString(shift));
        sb.append("&ctrl=");
        sb.append(Boolean.toString(ctrl));
        sb.append("&alt=");
        sb.append(Boolean.toString(alt));
        sb.append("&meta=");
        sb.append(Boolean.toString(meta));
        sb.append("&rawCode=");
        sb.append(rawCode);
        return sb.toString();
    }

    /**
     * Load the key mapping information from the file given by keyMappingFilePath.
     * <p>
     * AndroidAssembly1 changed parameter type
     *
     * @param inputStream the input stream of resource
     * @throws KeyMappingFileLoadingException if any error occurs
     */
    private synchronized void loadKeyMappingFile(InputStream inputStream) throws KeyMappingFileLoadingException {
        final String methodName = "loadKeyMappingFile()";

        keyMapping = new HashMap<String, XREVirtualKeyCode>();
        DocumentBuilderFactory factory = null;
        try {
            factory = DocumentBuilderFactory.newInstance();
        } catch (FactoryConfigurationError e) {
            throw LogUtil.error(LOGGER, this, methodName, new KeyMappingFileLoadingException(
                "Error! Some error occurred while creating DocumentBuilderFactory.", e));
        }
        factory.setNamespaceAware(true);
        DocumentBuilder builder = null;

        try {
            builder = factory.newDocumentBuilder();
        } catch (ParserConfigurationException e) {
            throw LogUtil.error(LOGGER, this, methodName, new KeyMappingFileLoadingException(
                "Error! Some error occurred while creating DocumentBuilder.", e));
        }

        Document doc = null;
        try {
            doc = builder.parse(inputStream);
        } catch (SAXException e) {
            throw LogUtil.error(LOGGER, this, methodName, new KeyMappingFileLoadingException(
                "Error! Some error occurred while parsing key mapping file.", e));
        } catch (IOException e) {
            throw LogUtil.error(LOGGER, this, methodName, new KeyMappingFileLoadingException(
                "Error! Some I/O error occurred while reading key mapping file.", e));
        }

        NodeList keys = doc.getElementsByTagName("key");
        for (int i = 0; i < keys.getLength(); i++) {
            Node key = keys.item(i);
            addKey(methodName, key);
        }
    }

    /**
     * <p>
     * Adds a key in the mapping.
     * </p>
     *
     * @param methodName the name of the method to be used in logs.
     * @param key the node that holds all informations of the key.
     * @throws KeyMappingFileLoadingException if any error occurs.
     */
    @SuppressWarnings("boxing")
    private void addKey(final String methodName, Node key) throws KeyMappingFileLoadingException {
        String rawCode = key.getTextContent();
        validateRawCode(rawCode);
        NamedNodeMap attributes = key.getAttributes();
        Node nameNode = attributes.getNamedItem("name");
        Node shiftNode = attributes.getNamedItem("shift");
        Node ctrlNode = attributes.getNamedItem("ctrl");
        Node altNode = attributes.getNamedItem("alt");
        Node metaNode = attributes.getNamedItem("meta");

        try {
            EventsUtil.checkArgumentNull(nameNode, "nameNode");
            EventsUtil.checkArgumentNull(shiftNode, "shiftNode");
            EventsUtil.checkArgumentNull(ctrlNode, "ctrlNode");
            EventsUtil.checkArgumentNull(altNode, "altNode");
            EventsUtil.checkArgumentNull(metaNode, "metaNode");
        } catch (IllegalArgumentException e) {
            throw LogUtil.error(LOGGER, this, methodName, new KeyMappingFileLoadingException(e.getMessage()));
        }

        boolean shift = Boolean.valueOf(shiftNode.getNodeValue());
        boolean ctrl = Boolean.valueOf(ctrlNode.getNodeValue());
        boolean alt = Boolean.valueOf(altNode.getNodeValue());
        boolean meta = Boolean.valueOf(metaNode.getNodeValue());

        String encodedCode = null;
        try {
            encodedCode = encodeKeyCode(Integer.decode(rawCode), shift, ctrl, alt, meta);
        } catch (NumberFormatException e) {
            throw LogUtil.error(LOGGER, this, methodName, new KeyMappingFileLoadingException(String.format(
                "Error! Raw code for key \"%s\" has invalid value.", key), e));
        }

        String name = nameNode.getNodeValue();
        XREVirtualKeyCode xreVirtualKeyCode = getXREVirtualKeyCode(name);

        this.keyMapping.put(encodedCode, xreVirtualKeyCode);
    }

    /**
     * <p>
     * Gets the XREVirtualKeyCode object stored in a static field of {@link XREVirtualKeyCode} class The static field
     * must have the given name.
     * </p>
     *
     * @param name the name of the static field to retrieve the object stored.
     * @return a XREVirtualKeyCode object.
     * @throws KeyMappingFileLoadingException if for some reason the object could not be retrieved.
     */
    private XREVirtualKeyCode getXREVirtualKeyCode(String name) throws KeyMappingFileLoadingException {
        final String methodName = "getXREVirtualKeyCode(String)";
        Field field = null;
        try {
            field = XREVirtualKeyCode.class.getDeclaredField(name);
        } catch (SecurityException e) {
            throw LogUtil.error(LOGGER, this, methodName, new KeyMappingFileLoadingException(
                "Error! Some security error occurred while getting XREVirtualKeyCode field: " + name, e));
        } catch (NoSuchFieldException e) {
            throw LogUtil.error(LOGGER, this, methodName, new KeyMappingFileLoadingException(String.format(
                "Error! Could not find declared field \"%s\" to get in XREVirtualKeyCode.", name), e));
        }

        try {
            return (XREVirtualKeyCode) field.get(null);
        } catch (IllegalArgumentException e) {
            throw LogUtil.error(LOGGER, this, methodName, new KeyMappingFileLoadingException(
                "Error! The declared field does not have proper type: " + name, e));
        } catch (IllegalAccessException e) {
            throw LogUtil.error(LOGGER, this, methodName, new KeyMappingFileLoadingException(
                "Error! Can not access declared field. Field is not accessible: " + name, e));
        }
    }

    /**
     * <p>
     * Checks if raw code has a valid hexadecimal value.
     * </p>
     *
     * @param rawCode the value to be checked.
     * @throws KeyMappingFileLoadingException if raw code has an invalid hexadecimal value.
     */
    private void validateRawCode(String rawCode) throws KeyMappingFileLoadingException {
        if (!RAW_CODE_PATTERN.matcher(rawCode).matches()) {
            throw new KeyMappingFileLoadingException("Error! Raw code has invalid hexadecimal value: " + rawCode);
        }
    }

    /**
     * <p>
     * This method is called right after the dependency of this class is fully injected. It checks if the injected
     * values are valid.
     * </p>
     *
     * @throws AndroidReceiverEventHandlersConfigurationException if any of the injected values is invalid.
     */
    public void checkInitialization() {
        final String methodName = "checkInitialization";

        LogUtil.enter(LOGGER, this, methodName);

        if (context == null) {
            throw LogUtil.error(LOGGER, this, methodName, new AndroidReceiverEventHandlersConfigurationException(
                "Argument context can not be null."));
        }

        LogUtil.exit(LOGGER, this, methodName);
    }
}
